package gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JToolBar;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import autotest.AutoTest;
import bebetes.Bebete;
import bebetes.BebeteEmergente;
import bebetes.BebeteHasard;
import bebetes.ChampDeBebetes;

/**
 * Classe permettant de lancer la simulation.
 * 
 * @author COLLET Philippe
 * @author gnpt
 */
public class LanceBebetes extends JFrame {
	private static final long serialVersionUID = 1L;
	// La barre d'outil permettant de gérer la simulation
	private JToolBar barreOutils;
	// Le champs de bébêtes affiché
	private ChampDeBebetes champs;
	// Slider permettant de régler le nombre d'images par seconde
	private JSlider fpsSlider;
	// Panneau de gauche contenant les actions et les statistiques
	private JPanel panneauGauche;
	// Panneau contenant les actions sur le champ
	private JPanel panneauChargeurBebetes;
	// Une Liste des types de bêbètes simulables
	private JList listeTypesBebetes;
	// Modèle par défaut associé à la liste des types de bébête
	private DefaultListModel listeTypeModele;
	// Panneau contenant les statistiques des tests passés
	private JEditorPane panneauResultatTests;
	// L'écouteur du LanceBebetes
	private LanceBebetesListener lanceBebeteListener;
	// Types de bébêtes disponibles
	private List<Class<? extends Bebete>> bebetesDisponibles;
	// Informations sur les tests passés sur les bebetesDisponibles
	private List<ResultatsTests> bebetesTestsResultats;
	// Informations sur les tests passés sur le champs
	private ResultatsTests champTestsResulats;
	// Informations sur les tests passés sur le système d'autotest
	private ResultatsTests autotestTestsResultats;

	/**
	 * Constructeur.
	 * 
	 * @param largeur
	 *            largeur de la fenêtre en pixels.
	 * @param hauteur
	 *            hauteur de la fenêtre en pixels.
	 */
	public LanceBebetes(int largeur, int hauteur) {
		// Type de bébête disponibles par défaut avec l'application
		this.bebetesDisponibles = new ArrayList<Class<? extends Bebete>>();
		this.bebetesDisponibles.add(BebeteEmergente.class);
		this.bebetesDisponibles.add(BebeteHasard.class);

		// On effectue les tests sur les types de bébête déjà chargées
		this.bebetesTestsResultats = this.effectuerTestsSurBebetesDisponibles();
		// On effectue les tests sur le champs
		this.champTestsResulats = this.effectuerTestsSurChamp();
		// On récupère le résultat des tests effectués sur le système d'autotest
		this.autotestTestsResultats = new ResultatsTests(AutoTest.class,
				AutoTest.instance().getResults());

		/*
		 * On récupère la liste des types de bébête ayant passées tous leurs
		 * tests et on crée un nombre aléatoire de ces bébêtes dans le champs
		 */
		this.champs = new ChampDeBebetes(largeur, hauteur, this
				.getTypeBebetesPassantTousLesTests(), 100);
		this.creerEtAfficherFenetre();
	}

	/**
	 * Permet de connaître tous les types de bébêtes disponibles ayant passant
	 * tous leurs tests.
	 * 
	 * @return tous les types de bébêtes disponibles ayant passant tous leurs
	 *         tests.
	 */
	public List<Class<? extends Bebete>> getTypeBebetesPassantTousLesTests() {
		List<Class<? extends Bebete>> typeBebetes = new ArrayList<Class<? extends Bebete>>();
		for (ResultatsTests btr : this.bebetesTestsResultats) {
			if (btr.areTestsReussis()) {
				typeBebetes.add(btr.getClasse().asSubclass(Bebete.class));
			}
		}

		return typeBebetes;
	}

	/**
	 * Lance les tests sur les bébêtes disponibles et renvoie une liste de
	 * {@link ResultatsTests}.
	 * 
	 * @return une liste de {@link ResultatsTests}.
	 */
	public List<ResultatsTests> effectuerTestsSurBebetesDisponibles() {
		List<ResultatsTests> listeResultatTests = new ArrayList<ResultatsTests>();

		for (Class<? extends Bebete> typeBebete : this.bebetesDisponibles) {
			listeResultatTests.add(new ResultatsTests(typeBebete, AutoTest
					.run(typeBebete)));
		}

		return listeResultatTests;
	}

	/**
	 * Ajoute un résultat des tests passés pour une bébête. Si les tests sont
	 * tous passés alors le type de bébête est ajouter aux bebetesDisponibles.
	 * 
	 * @param rt
	 *            le résultat des tests passé pour une bébête.
	 */
	public void ajouterResultatsTestsBebete(ResultatsTests rt) {
		this.bebetesTestsResultats.add(rt);
		this.majPanneauResultatsTests();
		if (rt.areTestsReussis()) {
			Class<? extends Bebete> klass = rt.getClasse().asSubclass(
					Bebete.class);
			this.ajouterJListNouveauTypeBebete(klass);
		}
	}

	/**
	 * Lance les tests sur le champ et renvoie un {@link ResultatsTests}.
	 * 
	 * @return un {@link ResultatsTests} contenant les résultats des tests
	 *         effectués.
	 */
	public ResultatsTests effectuerTestsSurChamp() {
		return new ResultatsTests(ChampDeBebetes.class, AutoTest
				.run(ChampDeBebetes.class));
	}

	/**
	 * Créé la fenêtre et l'affîche.
	 */
	private void creerEtAfficherFenetre() {
		super.addWindowListener(new java.awt.event.WindowAdapter() {
			@Override
			public void windowClosing(java.awt.event.WindowEvent evt) {
				exitForm(evt);
			}
		});

		this.barreOutils = this.creerBarreOutils();

		super.getContentPane().add(this.barreOutils, BorderLayout.EAST);
		super.getContentPane().add(this.champs, BorderLayout.CENTER);
		super.getContentPane().add(this.getPanneauGauche(), BorderLayout.WEST);

		this.majPanneauResultatsTests();

		super.setResizable(false);
		super.setTitle("Champ de bebêtes");
		super.pack();
		super.setLocationRelativeTo(null);
	}

	/**
	 * Créé le JEditorPane la liste des tests étant passés.
	 * 
	 * @return le JEditorPane contenant la liste des tests étant passés.
	 */
	private JEditorPane creerEditorPane() {
		JEditorPane jep = new JEditorPane();
		jep.setEditable(false);
		jep.setContentType("text/html");
		return jep;
	}

	/**
	 * Met à jour le panneau contenant le résultat des tests passés.
	 */
	private void majPanneauResultatsTests() {
		StringBuffer buf = new StringBuffer();
		buf.append("<html>");
		buf.append("<head>");
		buf.append("<style type=\"text/css\">");
		buf.append("body { background-color: #fbfbee;");
		buf.append("font-family: \"Trebuchet MS\";");
		buf.append("font-size: 10px; margin: 10px; padding: 0; }");
		buf.append("h2 { margin: 0 0 5px 0; }");
		buf.append(".echecs { color: blue; }");
		buf.append(".erreurs { color: red; }");
		buf.append(".testsNonReussis { color: red; ");
		buf.append("text-decoration: line-through; }");
		buf.append(".testsReussis { color: green; }");
		buf.append("</style>");
		buf.append("</head>");
		buf.append("<body>");
		buf.append("<h2>Résultats des tests</h2>");

		buf.append(this.getHtmlCategorieResultats("AutoTest",
				this.autotestTestsResultats));
		buf.append(this.getHtmlCategorieResultats("Bébêtes",
				this.bebetesTestsResultats));
		buf.append(this.getHtmlCategorieResultats("Champ de Bébête",
				this.champTestsResulats));

		buf.append("</body>");
		buf.append("</html>");
		this.champTestsResulats.getPourcentageTestsPasses();
		this.panneauResultatTests.setText(buf.toString());
	}

	/**
	 * Permet de créer le code html associé à une liste de
	 * {@link ResultatsTests}.
	 * 
	 * @param cat
	 *            le nom de la catégorie.
	 * @param res
	 *            la liste des {@link ResultatsTests}
	 * @return le code html associé à une liste de {@link ResultatsTests}.
	 */
	private String getHtmlCategorieResultats(String cat,
			List<ResultatsTests> res) {
		StringBuffer buf = new StringBuffer("<h3>" + cat + "</h3>");

		buf.append("<table width=\"100%\">");
		for (ResultatsTests rt : res) {
			buf.append("<tr>");
			buf.append("<td width=\"85%\">");

			if (!rt.areTestsReussis()) {
				buf.append("<span class=\"testsNonReussis\">");
			} else {
				buf.append("<span class=\"testsReussis\">");
			}

			// Classe dont on a effectué les tests
			buf.append(rt.getClasse().getCanonicalName());
			buf.append("</span>");
			buf.append("</td>");

			// Informations sur le nombre échecs/erreurs
			buf.append("<td class=\"echecs\">(");
			buf.append(rt.getNbEchecs() + ")</td> ");
			buf.append("<td class=\"erreurs\">(");
			buf.append(rt.getNbErreurs() + ")</td>");
			buf.append("<br />");
			buf.append("</tr>");
		}
		buf.append("</table>");

		return buf.toString();
	}

	/**
	 * Permet de créer le code html associé à un {@link ResultatsTests}.
	 * 
	 * @param cat
	 *            le nom de la catégorie.
	 * @param rt
	 *            le {@link ResultatsTests}
	 * @return le code html associé à un {@link ResultatsTests}.
	 */
	private String getHtmlCategorieResultats(String cat, ResultatsTests rt) {
		ArrayList<ResultatsTests> l = new ArrayList<ResultatsTests>();
		l.add(rt);
		return this.getHtmlCategorieResultats(cat, l);
	}

	/**
	 * Accesseur pour le panneau gauche contenant les actions sur le champ et
	 * les résultats des tests.
	 * 
	 * @return le JPanel en question.
	 */

	public JPanel getPanneauGauche() {
		if (this.panneauGauche == null) {
			this.panneauGauche = this.creerPanneauGauche();
		}
		return this.panneauGauche;
	}

	/**
	 * Accesseur pour la partie supérieur du panneau gauche contenant les
	 * actions sur le champ.
	 * 
	 * @return le JPanel en question.
	 */
	public JPanel getPanneauChargeurDeBebetes() {
		if (this.panneauChargeurBebetes == null) {
			this.panneauChargeurBebetes = this.creerPanneauChargeurDeBebetes();
		}
		return this.panneauChargeurBebetes;
	}

	/**
	 * Accesseur pour la partie inférieure du panneau gauche contenant les
	 * résultats des tests.
	 * 
	 * @return la JScrollPane en question.
	 */
	public JScrollPane getPanneauResultatTests() {
		return this.creerPanneauResultatTests();
	}

	/**
	 * Accesseur pour le LanceBebetesListener qui gère les événements liés à
	 * l'interface graphique.
	 * 
	 * @return le LanceBebetesListener associé à cette instance.
	 */
	public LanceBebetesListener getLanceBebeteListener() {
		if (this.lanceBebeteListener == null) {
			this.lanceBebeteListener = new LanceBebetesListener(this);
		}
		return this.lanceBebeteListener;
	}

	/**
	 * Initialise le ChargeurDeBebetes pour obtenir le .class contenant les
	 * bêbètes à charger.
	 */
	public void chargeBebetes() {
		ChargeurDeBebetes chargeur = new ChargeurDeBebetes(this);
		chargeur.chargerBebetes();
	}

	/**
	 * Ajoute un nouveau type de Bebete dans la JList.
	 * 
	 * @param klass
	 *            le nouveau type de bébête.
	 */
	public void ajouterJListNouveauTypeBebete(Class<? extends Bebete> klass) {
		this.listeTypeModele.addElement(klass);
	}

	/**
	 * Crée le paneau gauche en deux parties : Les actions sur le champ, et les
	 * résultats
	 * 
	 * @return le JPanel contenant ces éléments.
	 */
	private JPanel creerPanneauGauche() {
		JPanel panneauGauche = new JPanel(new BorderLayout());
		panneauGauche.add(this.getPanneauChargeurDeBebetes(),
				BorderLayout.NORTH);
		panneauGauche.add(this.getPanneauResultatTests(), BorderLayout.CENTER);
		return panneauGauche;
	}

	/**
	 * Crée le panneau gauche supérieur contenant les actions sur le champ
	 * 
	 * @return le JPanel contenant ces éléments.
	 */
	private JPanel creerPanneauChargeurDeBebetes() {
		// Le panel
		JPanel panneauActions = new JPanel(new BorderLayout());

		this.listeTypeModele = new DefaultListModel();
		// La combo box des bêbètes simulables
		this.listeTypesBebetes = new JList(this.listeTypeModele);
		this.listeTypesBebetes.setLayoutOrientation(JList.VERTICAL);

		// Ajout des Bebetes testées dans la JList
		for (ResultatsTests rt : this.bebetesTestsResultats) {
			if (rt.areTestsReussis()) {
				this.ajouterJListNouveauTypeBebete(rt.getClasse().asSubclass(
						Bebete.class));
			}
		}

		JScrollPane listScroller = new JScrollPane(this.listeTypesBebetes);
		listScroller.setPreferredSize(new Dimension(250, 80));

		// Le bouton pour ajouter dynamiquement des bêbètes
		JButton chargeurBebetes = new JButton("Charger");
		chargeurBebetes.addActionListener(this.getLanceBebeteListener());
		chargeurBebetes.setActionCommand("chargeurDeBebetes");

		// Le bouton pour initialiser
		JButton initBebetes = new JButton("Initialiser");
		initBebetes.addActionListener(this.getLanceBebeteListener());
		initBebetes.setActionCommand("initBebetes");

		// Ajout des composants
		panneauActions.add(listScroller, BorderLayout.NORTH);

		JPanel panneauBoutons = new JPanel();
		panneauBoutons.add(chargeurBebetes);
		panneauBoutons.add(initBebetes);

		panneauActions.add(panneauBoutons, BorderLayout.CENTER);

		return panneauActions;
	}

	/**
	 * Créé le JScrollPane contenant la liste des tests étant passés.
	 * 
	 * @return le JScrollPane contenant la liste des tests étant passés.
	 */
	private JScrollPane creerPanneauResultatTests() {
		this.panneauResultatTests = this.creerEditorPane();
		JScrollPane editorScrollPane = new JScrollPane(
				this.panneauResultatTests);
		editorScrollPane.setMinimumSize(new Dimension(300, 480));

		return editorScrollPane;
	}

	/**
	 * Crée la barre d'outils contenant les boutons de raccourci du simulateur.
	 */
	private JToolBar creerBarreOutils() {
		this.barreOutils = new JToolBar("Gestion de la simulation",
				JToolBar.VERTICAL);
		this.barreOutils.setMargin(new Insets(5, 0, 5, 0));

		// Récupération du chemin des images de la barre d'outils
		ImageIcon iconControlPlay = LanceBebetes.createImageIcon(
				"control_play.png", "Lancer la simulation");
		ImageIcon iconControlStop = LanceBebetes.createImageIcon(
				"control_stop.png", "Arrêter la simulation");
		ImageIcon iconControlSuspend = LanceBebetes.createImageIcon(
				"control_suspend.png", "Pauser/Reprendre la simulation");
		ImageIcon iconControlStep = LanceBebetes.createImageIcon(
				"control_step.png", "Avancer la simulation d'un pas");

		// Bouton pour démarrer l'évolution du champ
		JButton btnControlPlay = new JButton(iconControlPlay);
		btnControlPlay.setAlignmentX(CENTER_ALIGNMENT);
		btnControlPlay.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
		btnControlPlay.setMargin(new Insets(0, 0, 0, 0));
		btnControlPlay.setToolTipText("Lancer la simulation");
		btnControlPlay.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				LanceBebetes.this.champs.demarre();
			}
		});

		// Bouton pour arrêter l'évolution du champ
		JButton btnControlStop = new JButton(iconControlStop);
		btnControlStop.setAlignmentX(CENTER_ALIGNMENT);
		btnControlStop.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
		btnControlStop.setToolTipText("Arrêter la simulation");
		btnControlStop.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				LanceBebetes.this.champs.arrete();
			}
		});

		// Bouton pour suspendre/reprendre la simulation "Suspend"
		JButton btnControlSuspend = new JButton(iconControlSuspend);
		btnControlSuspend.setAlignmentX(CENTER_ALIGNMENT);
		btnControlSuspend
				.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
		btnControlSuspend.setToolTipText("Suspendre la simulation");
		btnControlSuspend.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				LanceBebetes.this.champs.pause();
			}
		});

		// Bouton pour avancer la simulation pas à pas
		JButton btnControlStep = new JButton(iconControlStep);
		btnControlStep.setAlignmentX(CENTER_ALIGNMENT);
		btnControlStep.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
		btnControlStep.setToolTipText("Avancer la simulation d'un pas");
		btnControlStep.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				LanceBebetes.this.champs.unPas();
			}
		});

		// Slider permettant de régler le nombre d'images par seconde
		this.fpsSlider = new JSlider(JSlider.VERTICAL, 1, 300, 10);
		this.fpsSlider.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
		this.fpsSlider.setToolTipText("Nombre d'images par seconde");
		this.fpsSlider.setValue((int) Math.ceil((1.0 / this.champs
				.getDelaiSimulation()) * 1000));
		this.fpsSlider.addChangeListener(new ChangeListener() {
			@Override
			public void stateChanged(ChangeEvent e) {
				JSlider source = (JSlider) e.getSource();
				int delai = (int) Math.ceil((1.0 / source.getValue()) * 1000);
				LanceBebetes.this.champs.setDelaiSimulation(delai);
				LanceBebetes.this.champs.setDelaiVisuel(delai);
			}
		});

		/**
		 * On oriente la JSlider en fonction de l'orientation de la barreOutils.
		 */
		this.barreOutils.addComponentListener(new ComponentAdapter() {
			@Override
			public void componentMoved(ComponentEvent evt) {
				int toolbarOrientation = ((JToolBar) evt.getSource())
						.getOrientation();

				if (toolbarOrientation == JToolBar.HORIZONTAL) {
					LanceBebetes.this.fpsSlider
							.setOrientation(JSlider.HORIZONTAL);
				} else if (toolbarOrientation == JToolBar.VERTICAL) {
					LanceBebetes.this.fpsSlider
							.setOrientation(JSlider.VERTICAL);
				}
			}
		});

		this.barreOutils.addSeparator(new Dimension(0, 7));
		this.barreOutils.add(btnControlPlay);
		this.barreOutils.addSeparator(new Dimension(0, 7));
		this.barreOutils.add(btnControlSuspend);
		this.barreOutils.addSeparator(new Dimension(0, 7));
		this.barreOutils.add(btnControlStop);
		this.barreOutils.addSeparator(new Dimension(0, 5));
		this.barreOutils.addSeparator();
		this.barreOutils.addSeparator(new Dimension(0, 5));
		this.barreOutils.add(btnControlStep);
		this.barreOutils.addSeparator();
		this.barreOutils.addSeparator(new Dimension(0, 5));
		this.barreOutils.add(this.fpsSlider);

		return this.barreOutils;
	}

	/**
	 * Crée une {@link ImageIcon} à partir du nom de l'image se trouvant dans le
	 * package {@code img/}.
	 * 
	 * @param name
	 *            Le nom de l'image qui servira à créer l'ImageIcon.
	 * @param alt
	 *            La légende associée à l'ImageIcon si le fichier associé à
	 *            {@code name} n'est pas trouvé. Ce paramètre est optionnel.
	 * @return l'image îcone crée.
	 */
	public static ImageIcon createImageIcon(String name, String... alt) {
		String legend = name;

		if (alt != null) {
			legend = alt[0];
		}

		return new ImageIcon(LanceBebetes.class.getResource("img/" + name),
				legend);
	}

	private void exitForm(java.awt.event.WindowEvent evt) {
		System.exit(0);
	}

	/**
	 * Méthode principale pour lancer l'application.
	 * 
	 * @param args
	 *            arguments passés au programme.
	 */
	public static void main(String args[]) {
		new LanceBebetes(640, 480).setVisible(true);
	}

	/**
	 * Accesseur pour le champ de bébêtes.
	 * 
	 * @return le champ de bébêtes.
	 */
	public ChampDeBebetes getChamp() {
		return this.champs;
	}

	/**
	 * Accesseur pour connaître les types de bébêtes disponibles.
	 * 
	 * @return les types de bébêtes disponibles.
	 */
	public List<Class<? extends Bebete>> getBebetesDisponibles() {
		return this.bebetesDisponibles;
	}

	/**
	 * Accesseur pour la JList contenant les types de bébêtes disponibles ayant
	 * passées les tests.
	 * 
	 * @return les types de bébêtes disponibles ayant passées les tests.
	 */
	public JList getListeTypesBebetes() {
		return this.listeTypesBebetes;
	}

	/**
	 * Mutateur pour les types de bébêtes disponibles.
	 * 
	 * @param bebetesDisponibles
	 *            les nouveaux types de bébêtes disponibles.
	 */
	public void setBebetesDisponibles(
			List<Class<? extends Bebete>> bebetesDisponibles) {
		this.bebetesDisponibles = bebetesDisponibles;
	}
}
